/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.EventSink;
import org.jtell.EventSinkException;
import org.jtell.config.EventSinkMetadata;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * <p>
 * <code>MethodInvokingEventSinkAdapter</code> is an implementation of the {@link EventSink} interface which invokes a
 * method on an application listener object using reflection.
 * </p>
 * <p>
 * Since each instance of this class must be associated with a single application listener object, it must store a
 * reference to that object. To prevent this from interfering with the client application's listener lifecycle (whatever
 * that may be), we store weak references to these objects. {@link #isValid()} looks at the weak reference when
 * determining whether the adapter is valid.
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access.
 * </p>
 */
public class MethodInvokingEventSink implements EventSink
{
    private static Log LOG = LogFactory.getLog(MethodInvokingEventSink.class);

    /**
     * <p>
     * Index of the method parameter through which the event is to be passed, or <code>-1</code> to not pass the event.
     * </p>
     */
    private final int m_eventParameterIndex;

    /**
     * <p>
     * Precalculated instance hash code.
     * </p>
     */
    private final int m_hashCode;

    /**
     * <p>
     * The application listener object.
     * </p>
     */
    private final Reference<Object> m_listenerReference;

    /**
     * <p>
     * The event sink metadata.
     * </p>
     */
    private final EventSinkMetadata m_metadata;

    /**
     * <p>
     * The method to invoke.
     * </p>
     */
    private final Method m_method;

    /**
     * <p>
     * Number of parameters taken by the method.
     * </p>
     */
    private final int m_parameterCount;

    /**
     * <p>
     * Target identifier string. 
     * </p>
     */
    private final String m_targetId;

    /**
     * <p>
     * Construct a {@link MethodInvokingEventSink} instance.
     * </p>
     *
     * @param method the method to invoke.
     * @param listener the application listener object upon which the method will be invoked.
     * @param metadata the event sink metadata.
     * @param eventParameterIndex the event parameter index, or <code>-1</code> to not pass the event.
     */
    public MethodInvokingEventSink(final Method method,
                                   final Object listener,
                                   final EventSinkMetadata metadata,
                                   final int eventParameterIndex)
    {
        super();
        Guard.notNull("method", method);
        Guard.notNull("listener", listener);
        Guard.notNull("metadata", metadata);

        m_eventParameterIndex = eventParameterIndex;
        m_metadata = metadata;
        m_method = method;

        m_listenerReference = new WeakReference<Object>(listener);
        m_parameterCount = method.getParameterTypes().length;
        m_hashCode = calculateHashCode();
        m_targetId = ReflectionUtils.getMethodSignature(method, true);
    }

    /* inherit javadoc */
    public boolean equals(final Object baseOther)
    {
        boolean result = false;
        if (this == baseOther)
        {
            result = true;
        }
        else if (null != baseOther && getClass() == baseOther.getClass())
        {
            final MethodInvokingEventSink other = (MethodInvokingEventSink) baseOther;
            if (m_hashCode == other.m_hashCode &&
                    m_eventParameterIndex == other.m_eventParameterIndex &&
                    m_parameterCount == other.m_parameterCount &&
                    m_metadata.equals(other.m_metadata) &&
                    m_method.equals(other.m_method))
            {
                final Object listener = m_listenerReference.get();
                if (null != listener && listener.equals(other.m_listenerReference.get()))
                {
                    result = true;
                }
            }
        }
        return result;
    }

    /* inherit javadoc */
    public int hashCode()
    {
        return m_hashCode;
    }

    /* inherit javadoc */
    public EventSinkMetadata getEventSinkMetadata()
    {
        return m_metadata;
    }

    /* inherit javadoc */
    public Object getOwner()
    {
        return m_listenerReference.get();
    }

    /* inherit javadoc */
    public String getTargetId()
    {
        return m_targetId;
    }

    /* inherit javadoc */
    public boolean isValid()
    {
        // We are valid if our application listener is still referenced elsewhere.
        return null != m_listenerReference.get();
    }

    /* inherit javadoc */
    public void notifyListener(final Object event)
    {
        // Get the application listener object.
        final Object listener = m_listenerReference.get();
        if (null == listener)
        {
            // Our listener has been abandoned.
            if (LOG.isDebugEnabled())
            {
                LOG.debug(String.format("Listener invalid for event [%s].", event));
            }
        }
        else
        {
            // Get the method--it's really impossible for it to have been GC'ed when its implementing listener is still
            // alive, so don't bother to check for null.
            try
            {
                if (0 == m_parameterCount)
                {
                    m_method.invoke(listener);
                }
                else
                {
                    final Object[] parameters = new Object[m_parameterCount];
                    if (-1 != m_eventParameterIndex)
                    {
                        parameters[m_eventParameterIndex] = event;
                    }
                    m_method.invoke(listener, parameters);
                }
            }
            catch (IllegalAccessException e)
            {
                throw new EventSinkException(String.format(
                        "Event sink method [%s] on listener [%s] is not accessible.", m_method, listener), e);
            }
            catch (InvocationTargetException e)
            {
                throw new EventSinkException(String.format(
                        "An exception was thrown by event sink method [%s] on listener [%s]: [%s].", m_method,
                        listener, e.getMessage()), e.getCause());
            }
        }
    }

    /**
     * <p>
     * Calculate the instance hash code.
     * </p>
     *
     * @return <code>int</code> instance hash code.
     */
    private int calculateHashCode()
    {
        int result;
        result = m_eventParameterIndex;
        result = 31 * result + m_listenerReference.get().hashCode();
        result = 31 * result + m_metadata.hashCode();
        result = 31 * result + m_method.hashCode();
        result = 31 * result + m_parameterCount;
        return result;
    }
}
